package pers.luofei.http.client.core;

import pers.luofei.http.client.annotations.SimpleHttp;
import pers.luofei.http.client.converter.*;

import javax.net.ssl.*;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Type;
import java.net.HttpURLConnection;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static java.net.HttpURLConnection.HTTP_OK;
import static pers.luofei.http.client.core.ContentType.FORM_DATA;

/**
 * @author luofei[fei12990@foxmail.com]
 * @date 2017/9/12
 */
public class HttpExecutor {

    public static final String DEFAULT_BOUNDRAY = "------" + SimpleHttp.NAME + "BoundaryjT4BOC2D7I70yhYD";

    private static final String CONTENT_TYPE_FORM_DATA = "multipart/form-data;boundary=" + DEFAULT_BOUNDRAY;

    private X509TrustManager x509TrustManager;

    private HostnameVerifier hostnameVerifier;

    private Map<ContentType, RequestBodyEncoder> encoders;

    HttpExecutor() {

        // trust all
        x509TrustManager = new X509TrustManager() {
            @Override
            public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
            }

            @Override
            public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
            }

            @Override
            public X509Certificate[] getAcceptedIssuers() {
                return null;
            }
        };
        hostnameVerifier = new HostnameVerifier() {
            @Override
            public boolean verify(String s, SSLSession sslSession) {
                return true;
            }
        };

        encoders = new HashMap<>();
        encoders.put(ContentType.JSON, new JsonBodyEncoder());
        encoders.put(ContentType.XML, new XmlBodyEncoder());
        encoders.put(ContentType.FORM_URLENCODED, new UrlEncodedBodyEncoder());
        encoders.put(FORM_DATA, new FormDataEncoder());
        encoders.put(ContentType.OTHER, new OtherTypeBodyEncoder());
    }

    public void setHostnameVerifier(HostnameVerifier hostnameVerifier) {
        this.hostnameVerifier = hostnameVerifier;
    }

    public void setX509TrustManager(X509TrustManager x509TrustManager) {
        this.x509TrustManager = x509TrustManager;
    }

    public Object execute(HttpURLConnection connection, RequestMetaInfo requestMetaInfo, RequestData requestData) throws Exception {

        if (requestMetaInfo.getProtocol() == HttpProtocol.HTTPS) {
            SSLContext sc = SSLContext.getInstance("SSL");
            sc.init(null, new TrustManager[]{x509TrustManager}, new SecureRandom());
            HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
            HttpsURLConnection.setDefaultHostnameVerifier(hostnameVerifier);
        }

        // default headers
        Map<String, String> defaultHeaders = requestMetaInfo.getDefaultHeaders();
        for (Map.Entry<String, String> entry : defaultHeaders.entrySet()) {
            connection.setRequestProperty(entry.getKey(), entry.getValue());
        }

        // request method
        connection.setRequestMethod(requestMetaInfo.getMethod());

        // set custom headers
        for (Map.Entry<String, String> header : requestData.getCustomHeaders().entrySet()) {
            connection.setRequestProperty(header.getKey(), header.getValue());
        }
        // set request Content-Type
        ContentType contentType = requestMetaInfo.getContentType();
        connection.setRequestProperty(ContentType.KEY, contentType == FORM_DATA ? CONTENT_TYPE_FORM_DATA : contentType.getValue());

        // set parameters
        List<InputStream> requestInputStream = new ArrayList<>();
        if (requestData.getRequestBody() != null) {
            requestInputStream.add(requestData.getRequestBody());
        } else {
            List<? extends InputStream> streams = encoders.get(contentType).encode(requestData.getRequestParameters());
            if (streams != null) {
                requestInputStream.addAll(streams);
            }
        }

        OutputStream out;
        if (requestInputStream != null && !requestInputStream.isEmpty()) {
            connection.setDoOutput(true);
            out = connection.getOutputStream();
            byte[] buf = new byte[1024];
            for (InputStream inputStream : requestInputStream) {
                if (inputStream == null) {
                    continue;
                }
                int bytes;
                while ((bytes = inputStream.read(buf)) != -1) {
                    out.write(buf, 0, bytes);
                }
                inputStream.close();
            }
            out.close();
        }

        Type returnType = requestMetaInfo.getReturnType();
        int responseCode = connection.getResponseCode();
        InputStream in = responseCode == HTTP_OK ? connection.getInputStream() : connection.getErrorStream();

        RequestMethodInfo response = requestMetaInfo.toRequestMethodInfo();
        if (requestData.getCallBack() != null) {
            response.setCallback(requestData.getCallBack());
        }
        if (requestData.getResponseBodyDecoder() != null) {
            response.setDecoder(requestData.getResponseBodyDecoder());
        }
        response.setResponseContentLength(connection.getContentLengthLong());
        Map<String, List<String>> responseHeaders = new HashMap<>();
        for (Map.Entry<String, List<String>> entry : connection.getHeaderFields().entrySet()) {
            String k = entry.getKey();
            if (k == null) {
                continue;
            }
            responseHeaders.put(k.toLowerCase(), entry.getValue());
        }
        response.setResponseHeaders(responseHeaders);
        response.getResponseContentType().setValue(connection.getContentType());
        response.setContentEncoding(connection.getContentEncoding());
        ResponseBodyDecoder decoder = response.getDecoder();
        Object result = decoder.decode(response, responseCode, in);

        if (InputStream.class != returnType) {
            connection.disconnect();
        }

        return result;
    }
}